package be.ac.ua.auctioneer;

import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.Vector;

import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.swing.event.EventListenerList;

import net.jini.core.entry.Entry;
import net.jini.core.entry.UnusableEntryException;
import net.jini.core.event.UnknownEventException;
import net.jini.core.lease.LeaseDeniedException;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.core.transaction.CannotCommitException;
import net.jini.core.transaction.CannotJoinException;
import net.jini.core.transaction.Transaction;
import net.jini.core.transaction.TransactionException;
import net.jini.core.transaction.TransactionFactory;
import net.jini.core.transaction.UnknownTransactionException;
import net.jini.core.transaction.server.CrashCountException;
import net.jini.core.transaction.server.TransactionManager;
import net.jini.core.transaction.server.TransactionManager.Created;
import net.jini.lookup.entry.Name;
import net.jini.space.JavaSpace;
import be.ac.ua.codebase.BankInterface;
import be.ac.ua.codebase.Bidding;
import be.ac.ua.codebase.BiddingEvent;
import be.ac.ua.codebase.CustomerInterface;
import be.ac.ua.codebase.JoinRequest;
import be.ac.ua.codebase.Product;
import be.ac.ua.codebase.ProductOffer;
import distrcomp.Address;
import distrcomp.ClientTrackerImpl;
import distrcomp.Shipment;
import distrcomp.ShippingQuoter;
import distrcomp.ShippingState;
import distrcomp.ShippingType;

public abstract class AuctioneerImplementation implements Runnable {
	private CustomerInterface seller;
	private UUID sellerBankAccountID;
	private UUID sellerBank;
	protected Product product;
	protected Double minAmount;
	protected Double maxAmount;
	protected Vector<Bidding> biddings = new Vector<Bidding>();
	private int prevBiddingsLength = 0;
	private EventListenerList listeners = new EventListenerList();
	private int prevListenersLength = 0;
	private List<String> newCustomers = new LinkedList<String>();
	private AuctioneerManager manager = null;
	private long nextSeqNumber = 0;
	protected long startTime = 0;
	protected long duration = 0;
	protected double highestBidding = 0;
	private ClientTrackerImpl tracker = null;

	public AuctioneerImplementation(AuctioneerManager manager, ProductOffer productOffer) throws IOException {
		this.manager = manager;
		this.seller = productOffer.seller;
		this.sellerBankAccountID = productOffer.bankAccountID;
		this.sellerBank = productOffer.bank;
		this.product = productOffer.product;
		this.minAmount = productOffer.minAmount;
		this.maxAmount = productOffer.maxAmount;
		this.startTime = new Date().getTime();
		if(productOffer.duration != null) {
			this.duration = productOffer.duration;
		}
		System.out.println("[AUCTIONEER] New auctioneer started");
	}
	
	public BankInterface getBank(UUID id) throws RemoteException {
		BankInterface bankInterface = null; 
		Entry[] attributes = new Entry[] {new Name(id.toString())}; 

		for (int i = 0; i < this.manager.fRegistrars.size(); i++) {
			BankInterface tmp = (BankInterface) this.manager.fRegistrars.get(i).lookup(new ServiceTemplate(null, null, attributes));
			if(tmp != null) {
				return tmp;
			}
		}
		
		System.out.println("[AUCTIONEER] No bank found");
		return null;
	}
	
	/**
	 * Restarts the auction. Can be used when something goes wrong with the payment
	 */
	
	protected void restart() {
		this.biddings = new Vector<Bidding>();
		this.prevBiddingsLength = 0;
		this.highestBidding = 0;
		this.startTime = System.currentTimeMillis();
		this.notifyCustomers("The auction for product " + this.product.getName() + " is restarted. Please join the auction again.");
		this.listeners = new EventListenerList();
		this.tracker = null;
	}
	
	/**
	 * Loops over all JavaSpaces once, taking all requests out of a space until no entry's are found. 
	 */
	protected void findCustomers() throws LeaseDeniedException, RemoteException, UnusableEntryException, TransactionException, InterruptedException {
		if(this.product != null) {
			this.prevListenersLength = this.listeners.getListenerCount();
			this.newCustomers = new LinkedList<String>();
			
			//Initialize transaction
			TransactionManager transManager = this.getTransactionManager();
			if(transManager != null) {
				net.jini.core.transaction.Transaction.Created created = TransactionFactory.create(transManager, 5000);
				Transaction trans = created.transaction;
				
				//Initialize entries
				JoinRequest entryToMatch = new JoinRequest(this.product.getID());
				JoinRequest gotEntry;
				
				//Find waiting customers
				Vector<JavaSpace> spaces = this.manager.getSpaces();
				if(spaces != null) {
					for(int i=0; i<spaces.size(); ++i) {
						gotEntry = null;
						do {
							gotEntry = (JoinRequest) spaces.get(i).takeIfExists(entryToMatch, trans, 5000);
							if(gotEntry != null) {
								System.out.println("[AUCTIONEER] New listener found");
								this.listeners.add(CustomerInterface.class, gotEntry.getListener());
								this.newCustomers.add(gotEntry.customerName);
							}
						} while(gotEntry != null);
					}
				}
				
				//Commit transaction
				trans.commit();
			}
		}
	}
	
	/**
	 * Loops over all JavaSpaces once, taking all biddings out of a space until no entry's are found. 
	 */
	protected void findBiddings() throws LeaseDeniedException, RemoteException, UnusableEntryException, TransactionException, InterruptedException {
		if(this.product != null) {
			this.prevBiddingsLength = this.biddings.size();
			
			//Initialize transaction
			TransactionManager transManager = this.getTransactionManager();
			if(transManager != null) {
				net.jini.core.transaction.Transaction.Created created = TransactionFactory.create(transManager, 5000);
				Transaction trans = created.transaction;
				
				//Initialize entries
				Bidding entryToMatch = new Bidding(this.product.getID());
				Bidding gotEntry;
				
				//Find waiting customers
				Vector<JavaSpace> spaces = this.manager.getSpaces();
				if(spaces != null) {
					for(int i=0; i<spaces.size(); ++i) {
						gotEntry = null;
						do {
							gotEntry = (Bidding) spaces.get(i).takeIfExists(entryToMatch, trans, 5000);
							if(gotEntry != null) {
								double amount = gotEntry.getAmount();
								System.out.println("[AUCTIONEER] New bidding found (€ "+ amount +")");
		
								//Add the bidding if the amount is larger than the previous highest bidding
								if((amount > this.highestBidding) && (amount > this.minAmount)) {
									this.biddings.add(gotEntry);
									this.highestBidding = amount;
								}
							}
						} while(gotEntry != null);
					}
					
					//Commit transaction
					trans.commit();
				}
			}
		}
	}
	
	/**
	 * Notifies the customers of a certain message
	 */
	protected void notifyCustomers(String msg) {
		BiddingEvent remoteEvent = null;
		
		if(this.listeners != null) {
			//Get listeners
			Object[] listeners = this.listeners.getListenerList();
			
			//Send out notifications
			for (int i=0; i < listeners.length; i += 2) {
			    if (listeners[i] == CustomerInterface.class) {
			    	CustomerInterface listener = (CustomerInterface) listeners[i+1];
			    	if (remoteEvent == null) {
			    		remoteEvent = new BiddingEvent("Auctioneer", 0, this.nextSeqNumber, null, msg); //Source object is just a string because communication goes via JavaSpace
			    	}
					try {
					    listener.notify(remoteEvent);
					    System.out.println("[AUCTIONEER] Notification sent");
					} catch(UnknownEventException e) {
					    System.out.println("[AUCTIONEER] Unknown event");
					} catch(RemoteException e) {
						System.out.println("[AUCTIONEER] Couldn't notify customer");
						this.listeners.remove(CustomerInterface.class, listener);
					}
			    }
			}
			this.nextSeqNumber++;
		}
	}
	
	protected List<String> getNewCustomers() {
		return this.newCustomers;
	}
	
	protected int customerSize() {
		return this.listeners.getListenerCount();
	}
	
	protected TransactionManager getTransactionManager() {
		TransactionManager transactionManager = null;
		
		Entry[] attributes = new Entry[1];
		attributes[0] = new Name ("Transaction Manager");
		Class[] classes = new Class[] {TransactionManager.class};
		
		Vector<ServiceRegistrar> registrars = this.manager.getRegistars();
		
		for (int i = 0; i < registrars.size(); i++) {
			try {
				TransactionManager tmp = (TransactionManager) registrars.get(i).lookup(new ServiceTemplate(null, classes, null));
				if (tmp != null) {
					transactionManager = tmp;
				}
			} catch(RemoteException e) {
				System.out.println("[AUCTIONEER] Error loading transaction manager");
			}
		}
		
		if (transactionManager == null) {
			System.out.println("[AUCTIONEER] TransactionManager not found!");
		}
		
		return transactionManager;
	}
	
	private void transferMoneyFrom(UUID bankFrom, UUID idFrom, double amount, Created leasedTransaction) throws UnknownTransactionException, CannotJoinException, CrashCountException, RemoteException, CannotCommitException, LeaseDeniedException {
		TransactionManager transactionManager = this.getTransactionManager();
		if(transactionManager != null) {
			this.getBank(bankFrom).debit(idFrom, amount, leasedTransaction.id);
			this.manager.getBank().credit(this.manager.getBankAccountID(), amount, leasedTransaction.id);
		}
	}
	
	private void transferMoneyTo(UUID bankTo, UUID idTo, double amount, Created leasedTransaction) throws UnknownTransactionException, CannotJoinException, CrashCountException, RemoteException, CannotCommitException, LeaseDeniedException {
		TransactionManager transactionManager = this.getTransactionManager();
		if(transactionManager != null) {
			this.manager.getBank().debit(this.manager.getBankAccountID(), amount, leasedTransaction.id);
			this.getBank(bankTo).credit(idTo, amount, leasedTransaction.id);
		}
	}
	
	protected void transferMoney(UUID bankBuyer, UUID idBuyer, double auctionCost, double productCost, double shippingCost) throws RemoteException, LeaseDeniedException, UnknownTransactionException, CannotJoinException, CrashCountException, CannotCommitException {
		TransactionManager transactionManager = this.getTransactionManager();
		if(transactionManager != null) {
			Created leasedTransaction = transactionManager.create(1000 * 60 * 5);
			
			this.transferMoneyFrom(bankBuyer, idBuyer, productCost+shippingCost, leasedTransaction);
			this.transferMoneyTo(this.sellerBank, this.sellerBankAccountID, productCost-auctionCost, leasedTransaction);
			
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			transactionManager.commit(leasedTransaction.id);
		}
	}
	
	protected boolean shipProduct(Address dest, double price) {
		try {
			ShippingType type = ShippingType.GROUND;
			distrcomp.Product prod = new distrcomp.Product(this.product.getName(), price, "EUR", false, 0, 0, 0, 0);
			
			//Get the shipment object from the server
			Registry registry = LocateRegistry.getRegistry(this.manager.getReg(), 1099, new SslRMIClientSocketFactory());
			ShippingQuoter s = (ShippingQuoter) registry.lookup("ShippingService");
			if(s != null) {
				Shipment shipment = s.ship(prod, this.manager.getAddress(), dest, type);
			
				//Request state updates
				ClientTrackerImpl tracker = new ClientTrackerImpl(shipment);
				s.startTracker(tracker, shipment.getID(), null);
				this.tracker = tracker;
				return true;
			} else {
				return false;
			}
		} catch(Exception e) {
			return false;
		}
	}
	
	protected double calcShippingPrice(Address dest, double price) {
		try {
			ShippingType type = ShippingType.GROUND;
			distrcomp.Product prod = new distrcomp.Product(this.product.getName(), price, "EUR", false, 0, 0, 0, 1000);
			
			//Get the shipment object from the server
			Registry registry = LocateRegistry.getRegistry(this.manager.getReg(), 1099, new SslRMIClientSocketFactory());
			ShippingQuoter s = (ShippingQuoter) registry.lookup("ShippingService");
			if(s != null) {
				double shippingPrice = s.calcPrice(prod, this.manager.getAddress(), dest, type);
				return shippingPrice;
			} else {
				return 0;
			}
		} catch(Exception e) {
			return 0;
		}
	}
	
	protected ShippingState getShippingState() throws RemoteException {
		if(this.tracker != null) {
			return this.tracker.getState();
		}
		return null;
	}
	
	public boolean hasProduct() {
		return (this.product != null);
	}
	
	public void setProduct(Product product) {
		this.product = product;
	}
	
	public boolean hasNewBiddings() {
		return this.prevBiddingsLength != this.biddings.size();
	}
	
	public boolean hasNewListeners() {
		return this.prevListenersLength != this.listeners.getListenerCount();
	}

	/**
	 * Abstract methods
	 */

	public abstract void run();
	public abstract double calculateAmount();
}
